package jadeCW;

import java.util.Vector;
import java.util.StringTokenizer;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.CyclicBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPANames;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;

public class HospitalAgent extends Agent {
	// agent-wide shared data
	private int totalAppointments = 0;
	private AID[] appointments;
	// booleans for synchronisation
	private boolean processingUpdate = false;
	
	protected void setup() {  	
		// read the number of appointments to allocate
	  	Object[] args = getArguments();
	  	if (args != null && args.length > 0) {
	  		try {
	  			totalAppointments = Integer.parseInt((String)args[0]);
		  	} catch (NumberFormatException e) {
		        System.err.println("HospitalAgent argument must be an integer");
		        System.exit(1);
		    }
	  	}
	  	appointments = new AID[totalAppointments];
		
		// Register the service
		String serviceName = "appointments-booking";
	  	System.out.println("HospitalAgent "+getLocalName()+" registering service \""+serviceName+"\" of type \"allocate-appointments\"");
	  	try {
	  		DFAgentDescription dfd = new DFAgentDescription();
	  		dfd.setName(getAID());
	  		ServiceDescription sd = new ServiceDescription();
	  		sd.setName(serviceName);
	  		sd.setType("allocate-appointments");
	  		// Agents that want to use this service need to "know" the allocate-appointments-ontology
	  		sd.addOntologies("allocate-appointments-ontology");
	  		// Agents that want to use this service need to "speak" the FIPA-SL language
	  		sd.addLanguages(FIPANames.ContentLanguage.FIPA_SL);
	  		dfd.addServices(sd);
	  		
	  		DFService.register(this, dfd);
	  	}
	  	catch (FIPAException fe) {
	  		fe.printStackTrace();
	  	}
	  	
	  	// add behaviours
	  	addBehaviour(new AllocateAppointment());
	  	addBehaviour(new RespondToQuery());
		addBehaviour(new RespondToProposal2());
		addBehaviour(new UpdateAppointments());		
	}
	
	// Put agent clean-up operations here
	  protected void takeDown() {
	    // Deregister from the yellow pages
	    try {
	      DFService.deregister(this);
	    }
	    catch (FIPAException fe) {
	      fe.printStackTrace();
	    }
	  	for(int i=0; i<totalAppointments; i++) {
	  		System.out.print(getLocalName()+": Appointment "+(i+1)+": ");
	  		if(appointments[i] != null) {
	  			System.out.println(appointments[i].getLocalName());
	  		} else {
	  			System.out.println("null");
	  		}
	  	}
	  }
	
	
	private class AllocateAppointment extends CyclicBehaviour {
		@Override
        public void action() {

                // Receive a request for an appointment.

				// Template structure for Message
				MessageTemplate msg_temp = MessageTemplate.and(MessageTemplate.MatchConversationId("allocate-appointments"),
						MessageTemplate.MatchPerformative(ACLMessage.REQUEST));
				ACLMessage request = myAgent.receive(msg_temp);
				
/*				If there are any appointments available (i.e. not all appointments have been allocated), then pick
				one (any) and agree to the request notifying the requesting agent of the appointment allocated to it.*/
				
				if(request != null) {					
                    // If there are any appointments available (i.e. not all appointments have been allocated), then pick
                    // one (any) and agree to the request notifying the requesting agent of the appointment allocated to it.
                    ACLMessage reply = request.createReply();                    
                                      
/*                    If there are any appointments available (i.e. not all appointments have been allocated), then pick
                    one (any)*/                                        
                    boolean available = false;                  
                    int i=0;
                    while (i<appointments.length) {

                    	if (appointments[i] == null)
                    	{
                    		available = true;                    		
                    		break;
                    	}
                    	
                    	i++;
                    }
                    
                    // If there are any appointments available (i.e. not all appointments have been allocated), then pick
                    // one (any) and agree to the request notifying the requesting agent of the appointment allocated to it                    
                    if(available) {
                            reply.setPerformative(ACLMessage.AGREE);                            
                            reply.setContent(String.valueOf(i+1));                             
                            // In case an appointment is allocated, update the parent agent's state as to which agents have which appointments.
                            appointments[i] = request.getSender();    
                            System.out.println(getLocalName()+": Replying AGREE to "+request.getSender().getLocalName()+". Appointment assigned is: "+reply.getContent());
                    } 
                    //If there are no appointments available, then refuse the request
                    else {
                            reply.setPerformative(ACLMessage.REFUSE);     
                            System.out.println(getLocalName()+": Replying REFUSE to "+request.getSender().getLocalName()+". Appointment: "+reply.getContent()+" cannot be assigned");
                    }
                    myAgent.send(reply);
				} else {
				    block();
				}
        }
	}
	
	private class RespondToQuery extends CyclicBehaviour {
		@Override
        public void action() {
/*			Receive a query regarding the owner of a certain appointment. */
			MessageTemplate msg_temp = MessageTemplate.and(MessageTemplate.MatchConversationId("owner-AID"),
					MessageTemplate.MatchPerformative(ACLMessage.REQUEST));
			ACLMessage query = myAgent.receive(msg_temp);
						
			if(query != null) {					
                ACLMessage reply = query.createReply();                    
                
                AID owner = null;
                try{
	                int desiredAppointment = Integer.parseInt(query.getContent().toString())-1;
	                
	                /*Respond with the owner of that appointment, if the appointment is held by an agent */
	                if(desiredAppointment<totalAppointments) {
	                	/*which could be a PatientAgent*/
		            	if (appointments[desiredAppointment]!=null) {
		            		owner = appointments[desiredAppointment];
		            	}
		            	/* or the parent agent of this behaviour.*/
		            	else {
		            		owner = myAgent.getAID();                		
		            	}
	                }
	                  
	                if(owner != null) {
	                    reply.setPerformative(ACLMessage.INFORM);
	                    reply.setContent(owner.getName());
	                    System.out.println(getLocalName()+": Replying INFORM owner to "+query.getSender().getLocalName()+". Owner is: "+reply.getContent());
	                }
	/*                Otherwise, notify the querying agent, for	example, that the owner of the appointment 
	 * 				  is not known, or the appointment is not one known of.*/
	                else {
	                    reply.setPerformative(ACLMessage.UNKNOWN);
	                    System.out.println(getLocalName()+": Replying UNKNOWN owner or Appointment to "+query.getSender().getLocalName());
	                }
	                myAgent.send(reply);
                } catch (NumberFormatException e) {
                	reply.setPerformative(ACLMessage.UNKNOWN);
                	System.out.println(getLocalName()+": Received non-integer appointment number \""+query.getContent()+"\" from "+query.getSender().getLocalName()+", Replying UNKNOWN");
                	myAgent.send(reply);
                }
			} 
		}
	}

	private class RespondToProposal2 extends CyclicBehaviour {
		@Override
        public void action() {
			if(!processingUpdate) {	// do not respond to swap proposals while the appointments table is being updated
				/*Receive a proposal to change appointments.*/
				MessageTemplate msg_temp = MessageTemplate.and(MessageTemplate.MatchConversationId("swap-proposal"),
						MessageTemplate.MatchPerformative(ACLMessage.PROPOSE));
				
				ACLMessage proposal = myAgent.receive(msg_temp);
							
				if(proposal != null) {					
	                ACLMessage reply = proposal.createReply();                    
	                AID owner = null;
	                
	                StringTokenizer st = new StringTokenizer(proposal.getContent());
					String str_proposedAppointment = st.nextToken();
					String str_wantedAppointment = st.nextToken();
					
	                int proposedAppointment = Integer.parseInt(str_proposedAppointment)-1;
	                int wantedAppointment = Integer.parseInt(str_wantedAppointment)-1;
	                
	                if(proposedAppointment<totalAppointments) {
	/*	        			Otherwise, if the appointment to
		        			be given away is not available, then respond with rejection, notifying the proposing agent of the
		        			owner of the requested resource.*/
			            	if (appointments[wantedAppointment]!=null) {
			            		owner = appointments[wantedAppointment];
			            		reply.setContent(owner.getName());
			            		reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
			                    System.out.println(getLocalName()+": Replying REJECT PROPOSAL to "+proposal.getSender().getLocalName()+". Owner is: "+reply.getContent());
			            	}
			            	else {
			            		// In case the booking is changed, update the parent agent's state as to which agents have which appointments.		            		
			            		int i = 0;
			            		while (i<appointments.length) {
			                    	if (proposal.getSender().equals(appointments[i]))
			                    	{
			                    		appointments[i] = null;                    		
			                    		break;
			                    	}
			                    	i++;		                    	
			                    }		            				            		
			            		appointments[wantedAppointment] = proposal.getSender();
			            		owner = appointments[wantedAppointment];
			                    reply.setPerformative(ACLMessage.ACCEPT_PROPOSAL);
			                    reply.setContent(owner.getName());		                    
			                    System.out.println(getLocalName()+": Replying ACCEPT PROPOSAL to SWAP with "+proposal.getSender().getLocalName());
			            	}
	                }
	                else{
	                	reply.setPerformative(ACLMessage.UNKNOWN);
	                    System.out.println(getLocalName()+": Replying UNKNOWN owner or Appointment to "+proposal.getSender().getLocalName());	
	                }
	                myAgent.send(reply);
				}
			} else {
				block();
			}
		}		
	}
	
	private class UpdateAppointments extends CyclicBehaviour {
		private int proposedAppointment = 0;
		private int currentAppointment = 0;
		private MessageTemplate msg_temp;
		private Vector<Notification> unmatchedUpdates = new Vector<Notification>();
		
		// Stores the necessary information from an unmatched proposal
		private class Notification {
			public AID proposer;
			public int current, proposed;
			public Notification(AID proposer, int current, int proposed) {
				this.proposer=proposer;
				this.current=current;
				this.proposed=proposed;
			}
		}
		
		@Override
        public void action() {
			/*Receive a proposal to change appointments.*/
			msg_temp = MessageTemplate.and(MessageTemplate.MatchConversationId("swap-appointments"),
					MessageTemplate.MatchPerformative(ACLMessage.INFORM));
			ACLMessage proposal = myAgent.receive(msg_temp);
			
			if((proposal != null)){
				processingUpdate = true;
                proposedAppointment = Integer.parseInt(proposal.getContent().toString())-1;
                
                if(proposedAppointment<totalAppointments) {
            		int i = 0;
            		// find the agent's current appointment
            		while (i<appointments.length) {
                    	if (proposal.getSender().equals(appointments[i])) {		                    		
                    		currentAppointment = i;                    		
                    		break;
                    	}		                    	
                    	i++;
                    }
            		boolean matched = false;
            		// search through the previously unmatched notifications for a valid swap
            		for(Notification n : unmatchedUpdates) {
            			if ((n.current==proposedAppointment)&&(n.proposed==currentAppointment)){
	            			appointments[proposedAppointment] = proposal.getSender();
	            			appointments[n.proposed] = n.proposer;
	            			matched = true;
	            			unmatchedUpdates.remove(n);
	            			System.out.println(getLocalName()+": Confirmed swap "+n.proposer.getLocalName()+"<->"+proposal.getSender().getLocalName()
	            					+", "+(proposedAppointment+1)+"<->"+(currentAppointment+1));
	            			break;
	            		}
            		}
            		// store the notification if unmatched
            		if(!matched) {
            			unmatchedUpdates.add(new Notification(proposal.getSender(), currentAppointment, proposedAppointment));
            		}
            		
            		//System.out.println(proposal.getSender().getLocalName()+" wants : "+(proposedAppointment+1));
            		//System.out.println(proposal.getSender().getLocalName()+" has : "+(currentAppointment+1));
                }
                else {
                	// The new appointment proposed does not exist
                	ACLMessage reply = proposal.createReply();
                	reply.setPerformative(ACLMessage.UNKNOWN);
                	System.out.println(getLocalName()+": UNKNOWN Appointment: "+reply.getContent());
                	myAgent.send(reply);
                	// NOTE: not currently processed by patientAgent
                }
                processingUpdate = false;
			}
		}
	}

}